package com.janetfilter.core.commons;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.janetfilter.core.utils.DateUtils;
import com.janetfilter.core.utils.ProcessUtils;
import com.janetfilter.core.utils.StringUtils;

public class DebugInfo {
	public static final long OUTPUT_CONSOLE = 0x1L;
	public static final long OUTPUT_FILE = 0x2L;
	public static final long OUTPUT_WITH_PID = 0x4L;

	private static final ExecutorService CONSOLE_EXECUTOR = Executors.newSingleThreadExecutor();
	private static final ExecutorService FILE_EXECUTOR = Executors.newSingleThreadExecutor();
	private static final String CLASS_NAME = DebugInfo.class.getName();
	private static final String LOG_TEMPLATE = "%s %-5s [%s@%-5s] %s-%d : %s%n";
	private static final String PID = ProcessUtils.currentId();
	private static final Level LOG_LEVEL;
	private static final Long LOG_OUTPUT;

	private static File logDir;

	static {
		Level level = Level.of(System.getProperty("janf.debug"));
		LOG_LEVEL = Level.NONE == level ? Level.of(System.getenv("JANF_DEBUG")) : level;

		Long output = StringUtils.toLong(System.getProperty("janf.output"));
		if (null == output) {
			output = StringUtils.toLong(System.getenv("JANF_OUTPUT"));
		}
		LOG_OUTPUT = null == output ? OUTPUT_CONSOLE : output;
	}

	public static void useFile(File dir) {
		if (Level.NONE == LOG_LEVEL || 0 == (LOG_OUTPUT & OUTPUT_FILE) || null == dir) {
			return;
		}

		if (!dir.exists() && !dir.mkdirs()) {
			error("Can't make directory: " + dir);
			return;
		}

		if (!dir.isDirectory()) {
			error("It's not a directory: " + dir);
			return;
		}

		if (!dir.canWrite()) {
			error("Read-only directory: " + dir);
			return;
		}

		logDir = dir;
	}

	public static Level getLogLevel() {
		return LOG_LEVEL;
	}

	public static long getLogOutput() {
		return LOG_OUTPUT;
	}

	public static void debug(String content, Throwable e) {
		output(Level.DEBUG, content, e);
	}

	public static void debug(String content) {
		debug(content, null);
	}

	public static void info(String content, Throwable e) {
		output(Level.INFO, content, e);
	}

	public static void info(String content) {
		info(content, null);
	}

	public static void warn(String content, Throwable e) {
		output(Level.WARN, content, e);
	}

	public static void warn(String content) {
		warn(content, null);
	}

	public static void error(String content, Throwable e) {
		output(Level.ERROR, content, e);
	}

	public static void error(String content) {
		error(content, null);
	}

	public static void output(String content) {
		debug(content);
	}

	public static void output(String content, Throwable e) { // No logger lib required
		debug(content, e);
	}

	public static void output(Level level, String content, Throwable e) { // No logger lib required
		if (Level.NONE == LOG_LEVEL || level.ordinal() < LOG_LEVEL.ordinal()) {
			return;
		}

		if (0 != (LOG_OUTPUT & OUTPUT_CONSOLE)) {
			CONSOLE_EXECUTOR.execute(new ConsoleWriteTask(PID, level, content, e));
		}

		if (null != logDir) {
			FILE_EXECUTOR.execute(new FileWriteTask(logDir, PID, level, content, e));
		}
	}

	public enum Level {
		NONE, DEBUG, INFO, WARN, ERROR;

		public static Level of(String valueStr) {
			if (null == valueStr) {
				return NONE;
			}

			int value;
			try {
				value = Integer.parseInt(valueStr);
			} catch (NumberFormatException e) {
				return NONE;
			}

			for (Level level : values()) {
				if (level.ordinal() == value) {
					return level;
				}
			}

			return NONE;
		}
	}

	private static class ConsoleWriteTask implements Runnable {
		private final String pid;
		private final Level level;
		private final String content;
		private final Throwable exception;
		private final Throwable stackException;
		private final String threadName;
		private final Date dateTime;

		private PrintStream ps;

		ConsoleWriteTask(String pid, Level level, String content, Throwable exception) {
			this.pid = pid;
			this.level = level;
			this.content = content;
			this.exception = exception;
			this.stackException = new Throwable();
			this.threadName = Thread.currentThread().getName();
			this.dateTime = new Date();

			setPrintStream(null == exception ? System.out : System.err);
		}

		protected static void writeContent(String content, PrintStream ps) {
			if (null == ps) {
				return;
			}

			ps.print(content);
		}

		protected static void writeException(String content, Throwable e, PrintStream ps) {
			if (null == ps) {
				return;
			}

			ps.print(content);
			e.printStackTrace(ps);
		}

		protected static void write(String content, Throwable e, PrintStream stream) {
			if (null == e) {
				writeContent(content, stream);
				return;
			}

			writeException(content, e, stream);
		}

		protected PrintStream getPrintStream() {
			return ps;
		}

		protected void setPrintStream(PrintStream ps) {
			this.ps = ps;
		}

		protected String getPID() {
			return pid;
		}

		public Date getDateTime() {
			return dateTime;
		}

		@Override
		public void run() {
			int line = 0;
			String caller = "UNKNOWN";
			StackTraceElement[] traces = stackException.getStackTrace();
			for (int i = 1, l = traces.length; i < l; i++) { // thank RayGicEFL
				StackTraceElement element = traces[i];
				if (!CLASS_NAME.equals(element.getClassName())) {
					line = element.getLineNumber();
					caller = element.getClassName();
					break;
				}
			}

			String outContent = String.format(LOG_TEMPLATE, DateUtils.formatDateTimeMicro(dateTime), level, threadName,
					pid, caller, line, content);
			write(outContent, exception, getPrintStream());
		}
	}

	private static class FileWriteTask extends ConsoleWriteTask {
		private final File logDir;

		FileWriteTask(File logDir, String pid, Level level, String content, Throwable exception) {
			super(pid, level, content, exception);

			this.logDir = logDir;
		}

		@Override
		public void run() {
			String fileName = String.format("%s%s.log", DateUtils.formatDate(getDateTime()),
					0 != (LOG_OUTPUT & OUTPUT_WITH_PID) ? "-" + getPID() : "");

			try (PrintStream ps = new PrintStream(new FileOutputStream(new File(logDir, fileName), true))) {
				setPrintStream(ps);

				super.run();
			} catch (FileNotFoundException e) {
				writeException("log file not found!", e, System.err);
			}
		}
	}
}
